vue2 与 vue3 变化总结


脚手架安装

我比较喜欢使用 vue-cli,在 vue3 中,使用安装脚手架比较简单,使用以下命令:

yarn global add @vue/cli
# 或
npm install -g @vue/cli

当然也有很多其他安装方式,比如 Vite,cdn,或者直接获取 vue 的库安装,详见官网

应用

创建

不同于 vue2 版本的 new Vue 语句,vue3 中创建应用实例如下:

const app = Vue.createApp({
  /* 选项 */
});

后续的注册全局指令,全局组件,或者使用路由和状态机都是链式调用,比如:

const instance = Vue.createApp({})
  .component("SearchInput", SearchInputComponent) // 全局组件
  .directive("focus", FocusDirective) // 全局指令
  .use(LocalePlugin) // 插件
  .use(router) // 路由,现在路由和状态机被当做插件使用
  .use(store) // 状态机
  .mount("#app"); // 挂载

生命周期

vue3 的声明周期相对 vue2 进行了一些修改,明显的就是 destory 被重命名为 unmount

官网图示

移除 $listeners

vue3 中奖不再有 $listeners,同样在模板中不能使用这个特殊变量。事件监听器现在是 $attrs 的一部分,万事大喜,以后 透传 只需要用 v-bind="$attrs" 方式即可:

<div v-bind="$attrs"></div>

不过这个还是不够灵活,比较很多时候我并不希望传递整个 $attrs

vue3 组件中的 this$attrs 都使用的是 Proxy 类型哦!不再是 Object.defineProperties 了。

模板语法

v-if 和 v-for 优先级更改

在 vue2 中,v-ifv-for 同时作用一个元素时,v-for 的优先级更高,在 vue3 中正好反过来。这对我影响倒是不大,因为 vue 任何版本都不推荐二者同时写到同一个元素上,所以我都是拆开写的。

多事件处理器

在迁移文档到我还没看到这个,vue3 中支持对一个事件传入多个处理器,使用逗号分开:

<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)">Submit</button>

按键修饰符

vue3 中:

  • 非兼容:不再支持使用数字 (即键码) 作为 v-on 修饰符

  • 非兼容:不再支持 config.keyCodes

不在支持键码了,现在你如果想监听字母 q 键,可以这么写:

<input type="text" @keypress.q="inputQ" />

当用户输入 q 时,将执行 inputQ 方法。

vue3 中按键修饰符语法是 kebab-cased (短横线) 名称官网示例很多。

用法还挺小清新,比如:

<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage" />

<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit" />

v-model

v-model 指令也大修了,如果在原生组件上使用 v-model,比如:

<input v-model="searchText" />

依旧和 vue2 一样,等价于:

<input :value="searchText" @input="searchText = $event.target.value" />

但是,在 vue3 中,如果在组件上使用 v-model,比如:

<my-input v-model="searchText" />

将变为:

<my-input :model-value="searchText" @update:model-value="searchText = $event"></my-input>

组件内需要绑定 v-model 将执行 $emit("update:modelValue",$event) 语法。

官网推荐这样写:

app.component("custom-input", {
  props: ["modelValue"],
  emits: ["update:modelValue"],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `,
});

移除 v-on.native 修饰符

在 vue3 中,修改了属性继承的定义。原生事件会直接透传到组件内部的根组件,不在使用 native 修饰符。详见

多个根节点的话就查看多个根节点上的 Attribute 继承

多个根组件还有个特殊情况

组件

emits 属性

emits 属性是新增的,类似 props,用来标记子组件可以香父组件传递的事件列表。

官网示例

自定义事件

自定义事件需要在 emits 属性中定义,参考 emits 属性

事件验证

自定义事件现在提供了验证功能,比如表单提交,你现在可以直接在用户点击提交按钮后触发自定义表单提交事件,验证部分可以交由事件验证功能处理。官网示例 验证抛出的事件

但是官网是这样说的:

验证函数应返回布尔值,以表示事件参数是否有效。

也就是不能异步验证,不能返回一个 Promise 当做验证结果,验证函数也不能是一个 async_function。

v-model 参数

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。

这个默认 prop 名和事件名是可以修改的,例如:

<my-component v-model:attrname="bookTitle"></my-component>

:attrname 将双向绑定的属性设置为 attrname,现在 attrname 将作为 prop 传给子组件,子组件可以通过 update:attrname 事件来修改该值。

子组件内:

import { defineComponent } from "vue";
export default defineComponent({
  name: "ChildComponent",
  emits: ['update:attrname']
  props: {
    attrname: String,
  },
  created() {
    this.$emit("update:attrname", "返给你一个初始值");
  },
});

官网示例示例

v-model 修饰符

vue3 中 v-model 也可以自定义修饰符了。

v-model 的修饰符将以 prop 的形式传递给子组件,类型例子如下:

// 修饰符例子
type ModifierExamples = "upper" | "lower";

// 默认 v-model 的修饰符 prop 名
type DefaultModifierProp = "modelModifiers";

// 默认 v-model 的修饰符 prop 值
type DefaultModifierPropValue = {
  [key in ModifierExamples]?: true;
};

// 自定义 v-model 的修饰符 prop 名,例如 v-model:custom
type CustomModifier = "custom";
type CustomModifierProp = `${CustomModifier}Modifiers`;

// 自定义 v-model:custom 的修饰符 prop 值
type CustomModifierPropValue = {
  [key in ModifierExamples]?: true;
};

例如上面的例子,我们传递给子组件的上下绑定如下:

<custome-el v-model.upper.lower="value" v-model:title.upper="title" />

那么在子组件中接收到的值为:

import { defineComponent } from "vue";

export default defineComponent({
  name: "CustomeEl",
  emits: {
    "update:modelValue": null,
    "update:title": null,
  },
  props: {
    modelValue: String,
    title: String,
    // v-model 的修饰符
    modelModifiers: {
      default: () => ({}),
    },
    // v-model:title 的修饰符
    titleModifiers: {
      default: () => ({}),
    },
  },
  created() {
    console.log(this.modelModifiers, this.titleModifiers);
    // 将会打印 {upper: true, lower: true} {upper: true}
  },
});

注意,没有传递的修饰符是接收不到的,接收到的必定是 true

Provide / Inject

这两个东西类似 react 中的上下文 Context,父组件可以使用 Provide 提供数据,子组件中可以使用 Inject 来注入数据。

官方文档在 vue 传统使用中对这个功能介绍很少,而且使用很变扭。

官方更推荐在响应式计算和侦听组合式 API 部分学习。

我在传统模式中使用了下,甚至有些意外的行为。而且传统模式下使用也很局限,还不如 vuex 方便。

异步组件

和 vue2 其实差别不大,不过现在的异步组件使用 defineAsyncComponent 来定义,比如:

import { createApp, defineAsyncComponent } from "vue";

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() => import("./components/AsyncComponent.vue")),
  },
});

另外特别重要的一点是,未来将推出 Suspense 功能,这个东西可以用来处理异步组件未加载完成时默认展示的内容,不过就是一个烟雾弹,看这里 Suspense

Suspense

这是一个实验性的功能,是拿来处理异步组件的。

react 的 concurrent 模式也是早就提出,一直没有落实下来,本来说 2020 年底推出,结果特么的现在都 2021 年 11 月了,还没推出。

vue3 的 Suspense 就是和 react 的 concurrent 差不多一个东西。

Suspense 就是用来处理异步组件的,不过看目前的进度,vue3 要等到 react 推出了 concurrent 模式,观摩之后再推出 Supense 吧。

组合式 API

现在每个组件都增加了一个叫 setup 的函数属性。

setup 会在组件未创建前执行,在 setup 内部,datacomputedmethodsrefs 都不可以使用。

返回一个对象

setup 可以返回一个对象,对象内部的属性都可以在 template 中使用。

示例:

<template>
  <div>
    <!-- 使用 setup return 对象的 count 属性 -->
    {{ count }}
  </div>
</template>

<script lang="ts">
  import { defineComponent } from "vue";

  export default defineComponent({
    setup() {
      // return 一个对象,其值可以在模板中使用
      return { count: 1 };
    },
  });
</script>

默认情况下,setup 中的数据不是响应式的,比如:

export default defineComponent({
  setup() {
    let count = 0;
    setInterval(() => count++, 1000);
    return { count };
  },
});

上面的代码中,在模板中使用 count 并不会更新。

返回渲染函数

setup 函数还可以返回一个渲染函数,如果使用 .vue 文件,那么在使用渲染函数的情况下,template 会失效,

渲染函数示例:

import { h, defineComponent } from "vue";

export default defineComponent({
  setup() {
    return () => h("div", {}, ["翻译翻译,什么叫惊喜?"]);
  },
});

你还可以使用 vue 官方提供的 jsx

import { defineComponent } from "vue";

export default defineComponent({
  setup() {
    return () => <div>翻译翻译,什么叫惊喜?</div>;
  },
});

啊这,好像很牛逼的样子。而且而且而且,在 vue3 中使用 jsx 还能使用 css scoped。这在 vue2 都没实现。

<script lang="tsx">
  import { defineComponent } from "vue";

  export default defineComponent({
    setup(props, context) {
      return () => <div class="root">翻译翻译,什么叫惊喜?</div>;
    },
  });
</script>
<style lang="sass" scoped>
  .root
    background: red
    color: white
</style>

不过我还是希望开放 jss,如果 emotionstyled-componentsmui 能够来 vue3 大显身手,岂不美哉。

setup 参数

setup 函数接收两个参数 propscontext

  • props:一个 Proxy 对象,内容是组件的 prop 所有值,不能直接解构,需要借助 toRef 或者 toRefs

  • context:一个普通对象,内容是组件上下文需要用到数据或函数,含有四个东西:

    • context.attrs:等同于 $attrsProxy 类型,不要解构使用。
    • context.slots:等同于 $slotsProxy 类型,不要解构使用。
    • context.emit:等同于 $emit
    • context.expose暴露公共 property 函数

    因为 context 不是 Proxy 对象,所以不是响应式的,可以直接解构。

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。请注意,与 props 不同,attrsslots 的 property 是非响应式的。如果你打算根据 attrsslots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

context.expose

expose 可以暴露一些公共方法到实例当中。使用 expose

例如封装了一个音频播放器组件,父组件希望通过组件实例来执行一个 play 方法,让播放器开始播放。此时就可以用到 expose

使用如:

export default {
  expose: ["play"],
  setup(props, { expose }) {
    //...

    expose({
      play() {
        console.log("音频开始播放!");
        // ...
      },
    });

    //...
  },
};

现在父组件就可以通过 $refs.refName.play() 来执行子组件通过 expose 暴露的函数了。

ref

reactive

toRefs

toRef

watchEffect

watchEffect 叫做监听器,和 react 的 useEffect 类似,但是不需要传入依赖项。

watchEffect 接收一个副作用函数(后文称作 effect) 和一个配置项(后文称作 options),返回一个停止器函数(后文称作 stoper)watchEffect 会立即执行一次接收到的 effect。

  • effectwatchEffect 会追踪 effect 内部的响应值作为 依赖项,当依赖项更新时,effect 会异步再次执行,默认情况下再次执行总是在生命周期 update 前。

  • options

    • options.flush

      • 默认值为 "pre" ,effect 会在更新前执行,effect 第一次会在立即执行。
      • 当值设置为 "post" 时,effect 会在更新后执行,effect 第一次会在组件挂载后在执行。
      • 当值设置为 "sync" 时,effect 的更新会在强制同步执行,这很低效并不推荐。

      从 Vue 3.2.0 开始,watchPostEffect 和 watchSyncEffect 别名也可以用来让代码意图更加明显。---官网

      话说 watchPostEffect 和 watchSyncEffect 很混淆好吧,还不如使用配置。---小编

    • options.onTrack & options.onTrigger:用来调试的,作用不大。

  • stoper:执行后 stoper 后会停止监听器,停止后 effect 不会因为依赖改变再次执行。

const count = ref(0);

const stoper = watchEffect(() => console.log(count.value));
// 依次打印 0 ,1 ,2 ,3

const tiemr = setInterval(() => {
  count.value++;

  // 值为 4 时停止
  if (count.value > 3) {
    stoper();
    clearInterval(tiemr);
  }
}, 100);

有时你需要在监听器失效时做一些处理,就像 react 的 useEffect 返回值一样功能,那么可以使用 onInvalidate

组合式 API 中的 Provide / Inject

在组合式 API 中使用 Provide / Inject 和 react 中的 Context 差不多,使用如下:

// 父组件
import { defineComponent, provide, ref } from "vue";
import Test from "./Test.vue";

export default defineComponent({
  name: "HelloWorld",
  components: { Test },
  setup() {
    const count = ref(0);
    const changeCount = (v) => (count.value = v);

    provide("countContext", { count, changeCount });
  },
});
// 子组件
import { defineComponent, inject, useCssModule } from "vue";

export default defineComponent({
  setup(props, { emit }) {
    const { count, changeCount } = inject("countContext" /* 可选默认值 */);

    return () => <div onClick={() => changeCount(count.value + 1)}>this is count :{count.value}</div>;
  },
});

onInvalidate

onInvalidate 用来设置监听器失效时的回调,需要把它写在 watchEffect 的副作用函数 effect 中。

onInvalidate 的失效有两种情况:

  • 副作用即将重新执行时

  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)

示例:

watchEffect((onInvalidate) => {
  const timer = setTimeout(fn, 3000);

  onInvalidate(() => {
    clearTimeout(timer);
  });
});

类似上面代码一样,使用 onInvalidate 可以在组件失效时,把一些过时的副作用清理掉。

watch

同 vue2 中的 watch 一样的功能,vue2 我就没咋用过这东西,详见官网

生命周期钩子

在组合式 API 中可以使用部分生命周期钩子。因为 setup 没有 this,所以这些钩子被封装在了全局上,并且其对应的名字如下:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

你可以通过下面方式调用:

import { defineComponent, onMounted, onBeforeMount } from "vue";

export default defineComponent({
  setup() {
    onMounted(() => console.log("组件挂载后"));
    onBeforeMount(() => console.log("组件挂载前"));
  },
});

模板引用

在组合式 API 中获取元素或组件实例

其他组合式 API

东西太多了,看这里

新增 Teleport,指定渲染位置

类似 react 的 Portals 功能,详见官网

全局 API

vue3 提供了很多新的全局 API,官网

单文件组件

参考

FAQ

vue3 中实现透传

参见 移除 $listeners